package com.lemon.exam.common.service.impl;

import com.aliyun.oss.model.PutObjectRequest;
import com.lemon.exam.common.constant.ApiConst;
import com.lemon.exam.common.entity.system.UploadFile;
import com.lemon.exam.common.enums.MinioBucketEnum;
import com.lemon.exam.common.enums.OssEnum;
import com.lemon.exam.common.exception.CustomException;
import com.lemon.exam.common.properties.AliyunProperties;
import com.lemon.exam.common.properties.ConfigProperties;
import com.lemon.exam.common.service.FileService;
import com.lemon.exam.common.util.RandomUtil;
import com.lemon.exam.common.util.RequestUtil;
import io.minio.*;
import io.minio.http.Method;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.io.ByteArrayInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDate;

/**
 * 文件上传实现类
 *
 * @author Lemon
 * @since 2025/4/16 11:22
 */
@Slf4j
@Service
public class FileServiceImpl implements FileService {
    @Resource
    private AliyunProperties aliyunProperties;
    @Resource
    private ConfigProperties configProperties;
    @Resource
    private MinioClient minioClient;
    @Resource
    private Environment environment;

    @Override
    public Mono<UploadFile> uploadToLocal(FilePart filePart) {
        return uploadToLocal(filePart, null);
    }

    @Override
    public Mono<UploadFile> uploadToLocal(FilePart filePart, String path) {
        return validateFile(filePart)
                .flatMap(part -> prepareFileInfo(part, path))
                .flatMap(file -> saveFileToDisk(filePart, file).thenReturn(file))
                .subscribeOn(Schedulers.parallel());
    }

    /**
     * 准备文件信息
     *
     * @param filePart
     * @param path
     * @return
     */
    private Mono<UploadFile> prepareFileInfo(FilePart filePart, String path) {
        return Mono.fromCallable(() -> {
            //1.原始文件名
            String originalName = filePart.filename();
            //2.文件扩展名
            String extension = extractFileExtension(originalName);
            //3.生成新的文件名
            String newName = RandomUtil.uuid().replace("-", "") + extension;
            //4.构建本地文件路径
            String url = String.format("http://%s:%s%s", RequestUtil.getServerIp(), environment.getProperty("server.port"), configProperties.getFileUpload().getMatch().replace("/**", ""));
            //5.构建路径
            String buildPath = String.format("%s/%s/%s", getPath(path), LocalDate.now().format(ApiConst.DATE_FORMATTER_10), newName);
            //6.构建本地文件路径
            String uploadPath = configProperties.getUploadPath(buildPath);
            return new UploadFile(originalName, newName, url, buildPath, uploadPath, null);
        }).subscribeOn(Schedulers.parallel());
    }

    /**
     * 获取路径
     *
     * @param path
     * @return
     */
    private static String getPath(String path) {
        if (!StringUtils.hasText(path) || "/".equals(path)) {
            return "";
        }
        if (!path.startsWith("/")) {
            path = "/" + path;
        }
        if (path.endsWith("/")) {
            path = path.substring(0, path.length() - 1);
        }
        return path;
    }

    /**
     * 保存文件到本地
     *
     * @param filePart
     * @param uploadFile
     * @return
     */
    private Mono<Void> saveFileToDisk(FilePart filePart, UploadFile uploadFile) {
        Path filePath = Paths.get(uploadFile.uploadPath());

        return Mono.fromCallable(() -> {
                    Files.createDirectories(filePath.getParent());
                    if (!filePath.normalize().startsWith(configProperties.getFileUpload().getResource())) {
                        throw new CustomException("文件路径无效");
                    }
                    return filePath;
                })
                .subscribeOn(Schedulers.boundedElastic())
                .flatMap(path -> DataBufferUtils
                        .write(filePart.content(), path, StandardOpenOption.CREATE, StandardOpenOption.WRITE)
                        .subscribeOn(Schedulers.boundedElastic()))
                .doOnError(e -> log.error("报错文件失败: {}", uploadFile.uploadPath(), e))
                .doOnSuccess(v -> log.info("文件保存成功: {}", uploadFile.uploadPath()));
    }

    @Override
    public Mono<UploadFile> uploadToOss(FilePart filePart, OssEnum ossEnum) {
        return validateFile(filePart)
                .flatMap(validatedFile -> prepareUploadFile(validatedFile, ossEnum))
                .flatMap(uploadFile -> readFileContent(filePart)
                        .flatMap(content -> uploadToOss(content, uploadFile, ossEnum))
                ).subscribeOn(Schedulers.parallel());
    }

    /**
     * 验证文件名字
     *
     * @param filePart
     * @return
     */
    private Mono<FilePart> validateFile(FilePart filePart) {
        return Mono.fromCallable(() -> {
            if (filePart == null) {
                throw new CustomException("未找到文件部分");
            }
            if (!StringUtils.hasText(filePart.filename())) {
                throw new CustomException("文件名不能为空");
            }
            return filePart;
        });
    }

    /**
     * 准备上传文件信息
     *
     * @param filePart
     * @param ossEnum
     * @return
     */
    private Mono<UploadFile> prepareUploadFile(FilePart filePart, OssEnum ossEnum) {
        return Mono.fromCallable(() -> {
            //1.原始文件名
            String originName = filePart.filename();
            //2.文件扩展名
            String extension = extractFileExtension(originName);
            //3.生成新的文件名
            String newName = RandomUtil.uuid().replace("-", "") + extension;
            //4.构建对象键
            String objectKey = String.format("%s/%s", ossEnum.getFolder().replaceAll("(^/+)|(/+$)", ""), newName);
            //5.构建OSS URL
            String url = String.format("https://%s.%s/%s", ossEnum.getBucket(), aliyunProperties.getOss().getEndpoint(), objectKey);
            return new UploadFile(originName, newName, url, null, null, objectKey);
        }).subscribeOn(Schedulers.parallel());
    }

    /**
     * 读取文件内容
     *
     * @param filePart
     * @return
     */
    private Mono<byte[]> readFileContent(FilePart filePart) {
        return filePart.content()
                .collectList()
                .map(dataBuffers -> {
                    int totalSize = dataBuffers.stream().mapToInt(DataBuffer::readableByteCount).sum();

                    byte[] bytes = new byte[totalSize];
                    int offset = 0;

                    for (DataBuffer buffer : dataBuffers) {
                        int count = buffer.readableByteCount();
                        buffer.read(bytes, offset, count);
                        offset += count;
                        DataBufferUtils.release(buffer); // 释放缓冲区
                    }
                    return bytes;
                });
    }

    /**
     * 上传到OSS
     *
     * @param fileContent
     * @param uploadFile
     * @param ossEnum
     * @return
     */
    private Mono<UploadFile> uploadToOss(byte[] fileContent, UploadFile uploadFile, OssEnum ossEnum) {
        return Mono.fromCallable(() -> {
                    aliyunProperties.getOss().oss().putObject(new PutObjectRequest(
                            ossEnum.getBucket(),
                            uploadFile.objectKey(),
                            new ByteArrayInputStream(fileContent)
                    ));
                    return uploadFile;
                })
                .subscribeOn(Schedulers.boundedElastic())
                .doOnError(e -> log.error("OSS上传失败", e));
    }


    /**
     * 提取文件扩展名
     *
     * @param filename
     * @return
     */
    private String extractFileExtension(String filename) {
        int lastDotIndex = filename.lastIndexOf(".");
        return lastDotIndex > 0 ? filename.substring(lastDotIndex) : "";
    }

    /**
     * 上传到MinIO
     *
     * @param filePart
     * @param bucketEnum
     * @return
     */
    @Override
    public Mono<UploadFile> uploadToMinio(FilePart filePart, MinioBucketEnum bucketEnum) {
        return validateFile(filePart)
                .flatMap(part -> prepareFileInfo(part, bucketEnum))
                .subscribeOn(Schedulers.parallel());
    }

    /**
     * 准备文件信息
     *
     * @param filePart
     * @param bucketEnum
     * @return
     */
    private Mono<UploadFile> prepareFileInfo(FilePart filePart, MinioBucketEnum bucketEnum) {
        return ensureBucketExists(bucketEnum).then(Mono.defer(() -> {
            String originalName = filePart.filename();
            String extension = extractFileExtension(originalName);
            String newName = RandomUtil.uuid().replace("-", "") + extension;
            return uploadFilePartToMinio(filePart, bucketEnum, newName, extension).map(url -> new UploadFile(originalName, newName, url));
        }));
    }

    private Mono<Boolean> ensureBucketExists(MinioBucketEnum bucketEnum) {
        return Mono.fromCallable(() -> {
            boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketEnum.getBucket()).build());
            if (!exists) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketEnum.getBucket()).build());
                log.info("Bucket '{}' created successfully", bucketEnum.getBucket());
            }
            return true;
        }).subscribeOn(Schedulers.boundedElastic());
    }

    /**
     * 上传文件部分到MinIO
     *
     * @param filePart
     * @param bucketEnum
     * @param newName
     * @param extension
     * @return
     */
    private Mono<String> uploadFilePartToMinio(FilePart filePart, MinioBucketEnum bucketEnum, String newName, String extension) {
        return DataBufferUtils.join(filePart.content()).flatMap(dataBuffer -> {
            try {
                byte[] fileContent = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(fileContent);
                DataBufferUtils.release(dataBuffer);

                return Mono.fromCallable(() -> {
                    minioClient.putObject(PutObjectArgs.builder()
                            .bucket(bucketEnum.getBucket())
                            .object(newName)
                            .stream(new ByteArrayInputStream(fileContent), fileContent.length, -1)
                            .contentType(getContentType(extension.toLowerCase()))
                            .build());

                    return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(bucketEnum.getBucket()).object(newName).build());
                }).subscribeOn(Schedulers.boundedElastic());
            } catch (Exception e) {
                DataBufferUtils.release(dataBuffer);
                return Mono.error(new CustomException("文件上传失败: " + e.getMessage()));
            }
        });
    }

    /**
     * 获取内容类型
     *
     * @param extension
     * @return
     */
    private String getContentType(String extension) {
        return switch (extension) {
            case ".jpg", ".jpeg" -> "image/jpeg";
            case ".png" -> "image/png";
            case ".gif" -> "image/gif";
            case ".pdf" -> "application/pdf";
            case ".doc", ".docx" -> "application/msword";
            case ".xls", ".xlsx" -> "application/vnd.ms-excel";
            case ".ppt", ".pptx" -> "application/vnd.ms-powerpoint";
            default -> "application/octet-stream";
        };
    }
}
